Skip to content

chore: effect rpc and layer based startup#929

Draft
juliusmarminge wants to merge 18 commits intomainfrom
effect-http-router
Draft

chore: effect rpc and layer based startup#929
juliusmarminge wants to merge 18 commits intomainfrom
effect-http-router

Conversation

@juliusmarminge
Copy link
Member

@juliusmarminge juliusmarminge commented Mar 11, 2026

What Changed

Why

UI Changes

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Note

Medium Risk
Refactors core server startup and WebSocket request handling to a new Effect RpcServer/HttpRouter composition, so regressions could break connectivity or endpoint behavior. Adds new path/attachment handling and command normalization that touches filesystem writes and validation logic.

Overview
Migrates the server’s WebSocket request/response handling from the legacy wsServer.ts switch/envelope path to Effect RPC routes in apps/server/src/ws.ts, wiring many existing WS_METHODS/orchestration procedures into a shared WsRpcGroup with typed errors and a new integration-heavy server.test.ts.

Restructures startup into a layer-based CLI entrypoint (src/bin.ts + src/cli.ts) and a new server.ts that composes HTTP routes (health, static/dev redirect, attachments) plus the WebSocket RPC route, and tightens filesystem safety via resolveWorkspaceWritePath and orchestration command normalization for attachment persistence.

Also centralizes several error types by re-exporting them from @t3tools/contracts, updates package bin/start scripts accordingly, and adds a .plans/ws-rpc-endpoint-port-plan.md plus formatter ignore for .reference.

Written by Cursor Bugbot for commit e3ac9ed. This will update automatically on new commits. Configure here.

Note

Replace WebSocket request handling with Effect RPC layer and restructure server startup

  • Introduces a typed WebSocket RPC API at /ws via WsRpcLayer in ws.ts, covering orchestration, git, terminal, project file ops, keybindings, and shell open. Contracts are defined in the new wsRpc.ts.
  • Replaces the previous ServerLive service/layer pattern with ServerLayer (a Layer.effectDiscard) and adds a CLI entrypoint in bin.ts with flag/env-based config resolution in cli.ts.
  • Moves HTTP routes (health, attachments, static/dev redirect) into dedicated layers in http.ts, assembled in server.ts.
  • Adds structured error types across contracts (GitManagerServiceError, TerminalError, OrchestrationDispatchCommandError, OpenError, etc.) used as typed RPC error channels.
  • Adds a reference server implementation under .reference/server/ as a working example of the Effect RPC + WebSocket pattern.
  • Risk: WebSocket requests previously handled in-process now return a RouteRequestError; clients must migrate to the /ws RPC endpoint.
📊 Macroscope summarized e3ac9ed. 38 files reviewed, 5 issues evaluated, 2 issues filtered, 1 comment posted

🗂️ Filtered Issues

.reference/server/src/model-store.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 218: Race condition in subscribe: Events published between catchup stream completion and live stream subscription start will be lost. Stream.concat(catchup, live) runs catchup first (line 210-212), and only after it completes does live (line 214-216) subscribe to eventsPubSub via Stream.fromPubSub. Any events published to eventsPubSub during this gap (e.g., by concurrent append calls on line 202) will not be delivered to the subscriber. [ Out of scope (triage) ]
packages/contracts/src/keybindings.test.ts — 0 comments posted, 2 evaluated, 1 filtered
  • line 53: Schema.decodeUnknownExit returns an Exit value directly, not an Effect. Using yield* on an Exit inside Effect.gen will fail because Exit is not a yieldable Effect. The code should either use Schema.decodeUnknownEffect (which returns an Effect) or wrap the Exit with something like Effect.fromExit. [ Failed validation ]

@juliusmarminge juliusmarminge marked this pull request as draft March 11, 2026 18:53
@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cae31266-4d9b-41df-bcbf-333a7c609423

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch effect-http-router

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Mar 11, 2026
Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Dev URL redirect drops request path and query
    • Fixed the redirect to construct a new URL from the request's pathname and search params against the dev origin, and updated the test to assert the correct behavior.

Create PR

Or push these changes by commenting:

@cursor push 089fb595df
Preview (089fb595df)
diff --git a/apps/server/src/http.ts b/apps/server/src/http.ts
--- a/apps/server/src/http.ts
+++ b/apps/server/src/http.ts
@@ -90,7 +90,8 @@
 
     const config = yield* ServerConfig;
     if (config.devUrl) {
-      return HttpServerResponse.redirect(config.devUrl.href, { status: 302 });
+      const devTarget = new URL(`${url.pathname}${url.search}`, config.devUrl);
+      return HttpServerResponse.redirect(devTarget.href, { status: 302 });
     }
 
     if (!config.staticDir) {

diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts
--- a/apps/server/src/server.test.ts
+++ b/apps/server/src/server.test.ts
@@ -266,7 +266,7 @@
       const response = yield* Effect.promise(() => fetch(url, { redirect: "manual" }));
 
       assert.equal(response.status, 302);
-      assert.equal(response.headers.get("location"), "http://127.0.0.1:5173/");
+      assert.equal(response.headers.get("location"), "http://127.0.0.1:5173/foo/bar");
     }).pipe(Effect.provide(NodeHttpServer.layerTest)),
   );


const config = yield* ServerConfig;
if (config.devUrl) {
return HttpServerResponse.redirect(config.devUrl.href, { status: 302 });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dev URL redirect drops request path and query

High Severity

The dev URL redirect uses config.devUrl.href without appending the incoming request's pathname or search params. Every GET request (e.g. /settings?tab=general) redirects to the dev server root (http://127.0.0.1:5173/) instead of the matching route (http://127.0.0.1:5173/settings?tab=general). The reference implementation correctly constructs new URL(url.pathname + url.search, origin). The test at line 269 asserts the broken behavior, masking the regression.

Additional Locations (1)
Fix in Cursor Fix in Web

Comment on lines +107 to +108
const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) =>
Option.getOrElse(Option.filter(flag, Boolean), () => envValue);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low src/cli.ts:107

resolveBooleanFlag filters the flag with Option.filter(flag, Boolean), which converts Some(false) to None because Boolean(false) is false. When the user explicitly passes --no-browser=false, the explicit false is ignored and falls back to envValue. Consider removing the filter so explicit false values are preserved.

-const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) =>
-  Option.getOrElse(Option.filter(flag, Boolean), () => envValue);
+const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) =>
+  Option.getOrElse(flag, () => envValue);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/cli.ts around lines 107-108:

`resolveBooleanFlag` filters the flag with `Option.filter(flag, Boolean)`, which converts `Some(false)` to `None` because `Boolean(false)` is `false`. When the user explicitly passes `--no-browser=false`, the explicit `false` is ignored and falls back to `envValue`. Consider removing the filter so explicit `false` values are preserved.

Evidence trail:
apps/server/src/cli.ts lines 107-108 (REVIEWED_COMMIT) - `resolveBooleanFlag` definition using `Option.filter(flag, Boolean)`; lines 131, 133-136, 137-140 show the function is used for `noBrowser`, `autoBootstrapProjectFromCwd`, and `logWebSocketEvents` flags.

stream: true,
});

export const WsRpcGroup = RpcGroup.make(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low src/wsRpc.ts:257

WsRpcGroup omits WsSubscribeOrchestrationDomainEventsRpc and WsSubscribeServerLifecycleRpc, which are defined in the same file and follow the same pattern as included subscription RPCs. If this group is used to register available endpoints, clients will be unable to subscribe to orchestration domain events or server lifecycle events.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/contracts/src/wsRpc.ts around line 257:

`WsRpcGroup` omits `WsSubscribeOrchestrationDomainEventsRpc` and `WsSubscribeServerLifecycleRpc`, which are defined in the same file and follow the same pattern as included subscription RPCs. If this group is used to register available endpoints, clients will be unable to subscribe to orchestration domain events or server lifecycle events.

Evidence trail:
packages/contracts/src/wsRpc.ts lines 212-218 (WsSubscribeOrchestrationDomainEventsRpc definition), lines 233-238 (WsSubscribeServerLifecycleRpc definition), lines 240-287 (WsRpcGroup definition which includes WsSubscribeTerminalEventsRpc at line 279 and WsSubscribeServerConfigRpc at line 280, but omits the two mentioned RPCs)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant